/*
 * Copyright (c) 2009 Christoph Studer <chstuder@gmail.com>
 * Copyright (c) 2010 Jan Berkel <jan.berkel@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.zegoggles.smssync;

import static com.zegoggles.smssync.App.LOCAL_LOGV;
import static com.zegoggles.smssync.App.TAG;

import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import org.apache.commons.io.IOUtils;
import org.apache.james.mime4j.codec.EncoderUtil;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.CallLog;
import android.provider.ContactsContract;
import android.provider.Contacts.ContactMethods;
import android.provider.Contacts.People;
import android.provider.Contacts.Phones;
import android.provider.ContactsContract.Contacts;
import android.text.TextUtils;
import android.util.Log;

import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mail.filter.Base64OutputStream;
import com.fsck.k9.mail.internet.MimeBodyPart;
import com.fsck.k9.mail.internet.MimeHeader;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.mail.internet.TextBody;
import com.zegoggles.smssync.PrefStore.AddressStyle;

public class CursorToMessage {

	// ContactsContract.CommonDataKinds.Email.CONTENT_URI
	public static final Uri ECLAIR_CONTENT_URI = Uri
			.parse("content://com.android.contacts/data/emails");

	// PhoneLookup.CONTENT_FILTER_URI
	public static final Uri ECLAIR_CONTENT_FILTER_URI = Uri
			.parse("content://com.android.contacts/phone_lookup");

	public enum DataType {
		MMS, SMS, CALLLOG
	}

	private static final String REFERENCE_UID_TEMPLATE = "<%s.%s@sms-backup-plus.local>";
	private static final String MSG_ID_TEMPLATE = "<%s@sms-backup-plus.local>";

	private static final boolean NEW_CONTACT_API = Integer
			.parseInt(Build.VERSION.SDK) >= Build.VERSION_CODES.ECLAIR;

	private static final String[] PHONE_PROJECTION = NEW_CONTACT_API ? new String[] {
			Contacts._ID, Contacts.DISPLAY_NAME }
			: new String[] { Phones.PERSON_ID, People.NAME, Phones.NUMBER };

	private static final String UNKNOWN_NUMBER = "unknown.number";
	private static final String UNKNOWN_EMAIL = "unknown.email";

	private static final int MAX_PEOPLE_CACHE_SIZE = 500;
	private final AddressStyle mStyle;

	private final Context mContext;
	private final Address mUserAddress;
	private final ThreadHelper threadHelper = new ThreadHelper();

	// simple LRU cache
	private final Map<String, PersonRecord> mPeopleCache = new LinkedHashMap<String, PersonRecord>(
			MAX_PEOPLE_CACHE_SIZE + 1, .75F, true) {
		@Override
		public boolean removeEldestEntry(Map.Entry eldest) {
			return size() > MAX_PEOPLE_CACHE_SIZE;
		}
	};

	private String mReferenceValue;
	private final boolean mMarkAsRead;
	private final boolean mPrefix;

	/** used for whitelisting specific contacts */
	private final ContactAccessor.GroupContactIds allowedIds;

	/** email headers used to record meta data */
	public interface Headers {
		String ID = "X-smssync-id";
		String ADDRESS = "X-smssync-address";
		String DATATYPE = "X-smssync-datatype";
		String TYPE = "X-smssync-type";
		String DATE = "X-smssync-date";
		String THREAD_ID = "X-smssync-thread";
		String READ = "X-smssync-read";
		String STATUS = "X-smssync-status";
		String PROTOCOL = "X-smssync-protocol";
		String SERVICE_CENTER = "X-smssync-service_center";
		String BACKUP_TIME = "X-smssync-backup-time";
		String VERSION = "X-smssync-version";
		String DURATION = "X-smssync-duration";
	}

	public CursorToMessage(Context ctx, String userEmail) {
		mContext = ctx;
		mUserAddress = new Address(userEmail);
		mMarkAsRead = PrefStore.getMarkAsRead(ctx);
		mReferenceValue = PrefStore.getReferenceUid(ctx);
		mPrefix = PrefStore.getMailSubjectPrefix(mContext);
		mStyle = PrefStore.getEmailAddressStyle(ctx);

		if (mReferenceValue == null) {
			mReferenceValue = generateReferenceValue();
			PrefStore.setReferenceUid(ctx, mReferenceValue);
		}

		switch (PrefStore.getBackupContactGroup(ctx).type) {
		case EVERYBODY:
			allowedIds = null;
			break;
		default:
			allowedIds = App.contacts().getGroupContactIds(ctx,
					PrefStore.getBackupContactGroup(ctx));
			if (LOCAL_LOGV)
				Log.v(TAG, "whitelisted ids for backup: " + allowedIds);
		}

		Log.d(TAG, String.format("using %s contacts API",
				NEW_CONTACT_API ? "new" : "old"));
	}

	public ConversionResult cursorToMessages(final Cursor cursor,
			final int maxEntries, DataType dataType) throws MessagingException {
		final String[] columns = cursor.getColumnNames();
		final ConversionResult result = new ConversionResult(dataType);
		do {
			final long date = cursor.getLong(cursor
					.getColumnIndex(SmsConsts.DATE));
			if (date > result.maxDate) {
				result.maxDate = date;
			}
			final Map<String, String> msgMap = new HashMap<String, String>(
					columns.length);
			for (int i = 0; i < columns.length; i++) {
				msgMap.put(columns[i], cursor.getString(i));
			}

			Message m = null;
			switch (dataType) {
			case SMS:
				m = messageFromMapSms(msgMap);
				break;
			case MMS:
				m = messageFromMapMms(msgMap);
				break;
			case CALLLOG:
				m = messageFromMapCallLog(msgMap);
				break;
			}
			if (m != null) {
				result.messageList.add(m);
				result.mapList.add(msgMap);
			}
		} while (result.messageList.size() < maxEntries && cursor.moveToNext());

		return result;
	}

	public ContentValues messageToContentValues(final Message message)
			throws IOException, MessagingException {
		if (message == null)
			throw new MessagingException("message is null");

		final ContentValues values = new ContentValues();
		switch (getDataType(message)) {
		case SMS:
			if (message.getBody() == null)
				throw new MessagingException("body is null");

			InputStream is = message.getBody().getInputStream();
			if (is == null) {
				throw new MessagingException(
						"body.getInputStream() is null for "
								+ message.getBody());
			}
			final String body = IOUtils.toString(is);
			final String address = getHeader(message, Headers.ADDRESS);
			values.put(SmsConsts.BODY, body);
			values.put(SmsConsts.ADDRESS, address);
			values.put(SmsConsts.TYPE, getHeader(message, Headers.TYPE));
			values
					.put(SmsConsts.PROTOCOL, getHeader(message,
							Headers.PROTOCOL));
			values.put(SmsConsts.SERVICE_CENTER, getHeader(message,
					Headers.SERVICE_CENTER));
			values.put(SmsConsts.DATE, getHeader(message, Headers.DATE));
			values.put(SmsConsts.STATUS, getHeader(message, Headers.STATUS));
			values.put(SmsConsts.THREAD_ID, threadHelper.getThreadId(mContext,
					address));
			values.put(SmsConsts.READ, PrefStore
					.getMarkAsReadOnRestore(mContext) ? "1" : getHeader(
					message, Headers.READ));
			break;
		case CALLLOG:
			values.put(CallLog.Calls.NUMBER,
					getHeader(message, Headers.ADDRESS));
			values.put(CallLog.Calls.TYPE, Integer.valueOf(getHeader(message,
					Headers.TYPE)));
			values.put(CallLog.Calls.DATE, getHeader(message, Headers.DATE));
			values.put(CallLog.Calls.DURATION, Long.valueOf(getHeader(message,
					Headers.DURATION)));
			values.put(CallLog.Calls.NEW, 0);

			PersonRecord record = lookupPerson(getHeader(message,
					Headers.ADDRESS));
			if (!record.unknown) {
				values.put(CallLog.Calls.CACHED_NAME, record.name);
				values.put(CallLog.Calls.CACHED_NUMBER_TYPE, -2);
			}

			break;
		default:
			throw new MessagingException("don't know how to restore "
					+ getDataType(message));
		}

		return values;
	}

	public DataType getDataType(Message message) {
		final String dataTypeHeader = getHeader(message, Headers.DATATYPE);
		final String typeHeader = getHeader(message, Headers.TYPE);
		// we have two possible header sets here
		// legacy: there is no CursorToMessage.Headers.DATATYPE.
		// CursorToMessage.Headers.TYPE
		// contains either the string "mms" or an integer which is the internal
		// type of the sms
		// current: there IS a Headers.DATATYPE containing a string
		// representation of CursorToMessage.DataType
		// CursorToMessage.Headers.TYPE then contains the type of the sms, mms
		// or calllog entry
		// The current header set was introduced in version 1.2.00
		if (dataTypeHeader == null) {
			return MmsConsts.LEGACY_HEADER.equalsIgnoreCase(typeHeader) ? DataType.MMS
					: DataType.SMS;
		} else {
			try {
				return DataType.valueOf(dataTypeHeader.toUpperCase());
			} catch (IllegalArgumentException e) {
				return DataType.SMS; // whateva
			}
		}
	}

	public static String formattedDuration(int duration) {
		return String.format("%02d:%02d:%02d", duration / 3600,
				duration % 3600 / 60, duration % 3600 % 60);
	}

	public String callTypeString(int callType, String name) {
		if (name == null) {
			return mContext
					.getString(callType == CallLog.Calls.OUTGOING_TYPE ? R.string.call_outgoing
							: callType == CallLog.Calls.INCOMING_TYPE ? R.string.call_incoming
									: R.string.call_missed);
		} else {
			return mContext
					.getString(
							callType == CallLog.Calls.OUTGOING_TYPE ? R.string.call_outgoing_text
									: callType == CallLog.Calls.INCOMING_TYPE ? R.string.call_incoming_text
											: R.string.call_missed_text, name);
		}
	}

	/* Look up a person */
	public PersonRecord lookupPerson(final String address) {
		if (!mPeopleCache.containsKey(address)) {
			Uri personUri = Uri.withAppendedPath(
					NEW_CONTACT_API ? ECLAIR_CONTENT_FILTER_URI
							: Phones.CONTENT_FILTER_URL, Uri.encode(address));

			Cursor c = mContext.getContentResolver().query(personUri,
					PHONE_PROJECTION, null, null, null);
			final PersonRecord record = new PersonRecord();
			if (c != null && c.moveToFirst()) {
				record._id = c.getLong(c.getColumnIndex(PHONE_PROJECTION[0]));
				record.name = sanitize(c.getString(c
						.getColumnIndex(PHONE_PROJECTION[1])));
				record.number = sanitize(NEW_CONTACT_API ? address : c
						.getString(c.getColumnIndex(PHONE_PROJECTION[2])));
				record.email = getPrimaryEmail(record._id, record.number);
			} else {
				if (LOCAL_LOGV)
					Log.v(TAG, "Looked up unknown address: " + address);

				record.number = sanitize(address);
				record.email = getUnknownEmail(address);
				record.unknown = true;
			}
			mPeopleCache.put(address, record);

			if (c != null)
				c.close();
		}
		return mPeopleCache.get(address);
	}

	private Message messageFromMapSms(Map<String, String> msgMap)
			throws MessagingException {
		final String address = msgMap.get(SmsConsts.ADDRESS);
		if (address == null || address.trim().length() == 0) {
			return null;
		}
		PersonRecord record = lookupPerson(address);
		if (!backupPerson(record, DataType.SMS))
			return null;

		final Message msg = new MimeMessage();
		msg.setSubject(getSubject(DataType.SMS, record));
		msg.setBody(new TextBody(msgMap.get(SmsConsts.BODY)));

		final int messageType = Integer.valueOf(msgMap.get(SmsConsts.TYPE));
		if (SmsConsts.MESSAGE_TYPE_INBOX == messageType) {
			// Received message
			msg.setFrom(record.getAddress());
			msg.setRecipient(RecipientType.TO, mUserAddress);
		} else {
			// Sent message
			msg.setRecipient(RecipientType.TO, record.getAddress());
			msg.setFrom(mUserAddress);
		}

		try {
			final Date then = new Date(Long.valueOf(msgMap.get(SmsConsts.DATE)));
			msg.setSentDate(then);
			msg.setInternalDate(then);
			msg.setHeader("Message-ID", createMessageId(then, address,
					messageType));
		} catch (NumberFormatException n) {
			Log.e(TAG, "error parsing date", n);
		}

		// Threading by person ID, not by thread ID. I think this value is more
		// stable.
		msg.setHeader("References", String.format(REFERENCE_UID_TEMPLATE,
				mReferenceValue, sanitize(record.getId())));
		msg.setHeader(Headers.ID, msgMap.get(SmsConsts.ID));
		msg.setHeader(Headers.ADDRESS, sanitize(address));
		msg.setHeader(Headers.DATATYPE, DataType.SMS.toString());
		msg.setHeader(Headers.TYPE, msgMap.get(SmsConsts.TYPE));
		msg.setHeader(Headers.DATE, msgMap.get(SmsConsts.DATE));
		msg.setHeader(Headers.THREAD_ID, msgMap.get(SmsConsts.THREAD_ID));
		msg.setHeader(Headers.READ, msgMap.get(SmsConsts.READ));
		msg.setHeader(Headers.STATUS, msgMap.get(SmsConsts.STATUS));
		msg.setHeader(Headers.PROTOCOL, msgMap.get(SmsConsts.PROTOCOL));
		msg.setHeader(Headers.SERVICE_CENTER, msgMap
				.get(SmsConsts.SERVICE_CENTER));
		msg.setHeader(Headers.BACKUP_TIME, new Date().toGMTString());
		msg.setHeader(Headers.VERSION, PrefStore.getVersion(mContext, true));
		msg.setFlag(Flag.SEEN, mMarkAsRead);

		return msg;
	}

	private Message messageFromMapCallLog(Map<String, String> msgMap)
			throws MessagingException {
		final String address = msgMap.get(CallLog.Calls.NUMBER);
		final int callType = Integer.parseInt(msgMap.get(CallLog.Calls.TYPE));

		if (address == null || address.trim().length() == 0
				|| !PrefStore.isCallLogTypeEnabled(mContext, callType)) {

			if (LOCAL_LOGV)
				Log.v(TAG, "ignoring call log entry: " + msgMap);
			return null;
		}

		PersonRecord record = lookupPerson(address);
		if (!backupPerson(record, DataType.CALLLOG))
			return null;

		final Message msg = new MimeMessage();
		msg.setSubject(getSubject(DataType.CALLLOG, record));

		switch (callType) {
		case CallLog.Calls.OUTGOING_TYPE:
			msg.setFrom(mUserAddress);
			msg.setRecipient(RecipientType.TO, record.getAddress());
			break;
		case CallLog.Calls.MISSED_TYPE:
		case CallLog.Calls.INCOMING_TYPE:
			msg.setFrom(record.getAddress());
			msg.setRecipient(RecipientType.TO, mUserAddress);
			break;

		default:
			// some weird phones seem to have SMS in their call logs, which is
			// not part of the official API.
			Log.i(TAG, "ignoring unknown call type: " + callType);
			return null;
		}

		final int duration = msgMap.get(CallLog.Calls.DURATION) == null ? 0
				: Integer.parseInt(msgMap.get(CallLog.Calls.DURATION));
		final StringBuilder text = new StringBuilder();

		if (callType != CallLog.Calls.MISSED_TYPE) {
			text.append(duration).append("s").append(" (").append(
					formattedDuration(duration)).append(")").append("\n");
		}
		text.append(record.getNumber()).append(" (").append(
				callTypeString(callType, null)).append(")");

		msg.setBody(new TextBody(text.toString()));

		try {
			Date then = new Date(Long.valueOf(msgMap.get(CallLog.Calls.DATE)));
			msg.setSentDate(then);
			msg.setInternalDate(then);
			msg.setHeader("Message-ID",
					createMessageId(then, address, callType));
		} catch (NumberFormatException n) {
			Log.e(TAG, "error parsing date", n);
		}

		// Threading by person ID, not by thread ID. I think this value is more
		// stable.
		msg.setHeader("References", String.format(REFERENCE_UID_TEMPLATE,
				mReferenceValue, sanitize(record.getId())));
		msg.setHeader(Headers.ID, msgMap.get(CallLog.Calls._ID));
		msg.setHeader(Headers.ADDRESS, sanitize(address));
		msg.setHeader(Headers.DATATYPE, DataType.CALLLOG.toString());
		msg.setHeader(Headers.TYPE, msgMap.get(CallLog.Calls.TYPE));
		msg.setHeader(Headers.DATE, msgMap.get(CallLog.Calls.DATE));
		msg.setHeader(Headers.DURATION, msgMap.get(CallLog.Calls.DURATION));
		msg.setHeader(Headers.BACKUP_TIME, new Date().toGMTString());
		msg.setHeader(Headers.VERSION, PrefStore.getVersion(mContext, true));
		msg.setFlag(Flag.SEEN, mMarkAsRead);

		return msg;
	}

	private boolean backupPerson(PersonRecord record, DataType type) {
		switch (type) {
		default:
			final boolean backup = (allowedIds == null || allowedIds.ids
					.contains(record._id));
			if (LOCAL_LOGV && !backup)
				Log.v(TAG, "not backing up " + type + " / " + record);
			return backup;
		}
	}

	private String getSubject(DataType type, PersonRecord record) {
		switch (type) {
		case SMS:
			return mPrefix ? String.format("[%s] %s", PrefStore
					.getImapFolder(mContext), record.getName()) : mContext
					.getString(R.string.sms_with_field, record.getName());
		case MMS:
			return mPrefix ? String.format("[%s] %s", PrefStore
					.getImapFolder(mContext), record.getName()) : mContext
					.getString(R.string.mms_with_field, record.getName());
		case CALLLOG:
			return mPrefix ? String.format("[%s] %s", PrefStore
					.getCallLogFolder(mContext), record.getName()) : mContext
					.getString(R.string.call_with_field, record.getName());
		default:
			throw new RuntimeException("unknown type:" + type);
		}
	}

	private Message messageFromMapMms(Map<String, String> msgMap)
			throws MessagingException {
		if (LOCAL_LOGV)
			Log.v(TAG, "messageFromMapMms(" + msgMap + ")");

		final Uri msgRef = Uri.withAppendedPath(ServiceBase.MMS_PROVIDER,
				msgMap.get(MmsConsts.ID));
		Cursor curAddr = mContext.getContentResolver().query(
				Uri.withAppendedPath(msgRef, "addr"), null, null, null, null);

		// TODO: this is probably not the best way to determine if a message is
		// inbound or outbound
		boolean inbound = true;
		final List<String> recipients = new ArrayList<String>(); // MMS
																	// recipients
		while (curAddr != null && curAddr.moveToNext()) {
			final String address = curAddr.getString(curAddr
					.getColumnIndex("address"));
			// final int type = curAddr.getInt(curAddr.getColumnIndex("type"));

			if (MmsConsts.INSERT_ADDRESS_TOKEN.equals(address)) {
				inbound = false;
			} else {
				recipients.add(address);
			}
		}
		if (curAddr != null)
			curAddr.close();
		if (recipients.isEmpty()) {
			Log.w(TAG, "no recipients found");
			return null;
		}

		final String address = recipients.get(0);
		final PersonRecord[] records = new PersonRecord[recipients.size()];
		final Address[] addresses = new Address[recipients.size()];
		for (int i = 0; i < recipients.size(); i++) {
			records[i] = lookupPerson(recipients.get(i));
			addresses[i] = records[i].getAddress();
		}

		boolean backup = false;
		for (PersonRecord r : records) {
			if (backupPerson(r, DataType.MMS)) {
				backup = true;
				break;
			}
		}
		if (!backup)
			return null;

		final Message msg = new MimeMessage();
		msg.setSubject(getSubject(DataType.MMS, records[0]));
		final int msg_box = Integer.parseInt(msgMap.get("msg_box"));
		if (inbound) {
			// msg_box == MmsConsts.MESSAGE_BOX_INBOX does not work
			msg.setFrom(records[0].getAddress());
			msg.setRecipient(RecipientType.TO, mUserAddress);
		} else {
			msg.setRecipients(RecipientType.TO, addresses);
			msg.setFrom(mUserAddress);
		}

		try {
			Date then = new Date(1000 * Long
					.valueOf(msgMap.get(MmsConsts.DATE)));
			msg.setSentDate(then);
			msg.setInternalDate(then);
			msg
					.setHeader("Message-ID", createMessageId(then, address,
							msg_box));
		} catch (NumberFormatException n) {
			Log.e(TAG, "error parsing date", n);
		}

		// Threading by person ID, not by thread ID. I think this value is more
		// stable.
		msg.setHeader("References", String.format(REFERENCE_UID_TEMPLATE,
				mReferenceValue, sanitize(records[0].getId())));
		msg.setHeader(Headers.ID, msgMap.get(MmsConsts.ID));
		msg.setHeader(Headers.ADDRESS, sanitize(address));
		msg.setHeader(Headers.DATATYPE, DataType.MMS.toString());
		msg.setHeader(Headers.TYPE, msgMap.get(MmsConsts.TYPE));
		msg.setHeader(Headers.DATE, msgMap.get(MmsConsts.DATE));
		msg.setHeader(Headers.THREAD_ID, msgMap.get(MmsConsts.THREAD_ID));
		msg.setHeader(Headers.READ, msgMap.get(MmsConsts.READ));
		msg.setHeader(Headers.BACKUP_TIME, new Date().toGMTString());
		msg.setHeader(Headers.VERSION, PrefStore.getVersion(mContext, true));
		msg.setFlag(Flag.SEEN, mMarkAsRead);

		// deal with attachments
		MimeMultipart body = new MimeMultipart();
		for (BodyPart p : getBodyParts(Uri.withAppendedPath(msgRef, "part"))) {
			body.addBodyPart(p);
		}
		msg.setBody(body);
		return msg;
	}

	private List<BodyPart> getBodyParts(final Uri uriPart)
			throws MessagingException {
		final List<BodyPart> parts = new ArrayList<BodyPart>();
		Cursor curPart = mContext.getContentResolver().query(uriPart, null,
				null, null, null);

		// _id, mid, seq, ct, name, chset, cd, fn, cid, cl, ctt_s, ctt_t, _data,
		// text
		while (curPart != null && curPart.moveToNext()) {
			final String id = curPart.getString(curPart.getColumnIndex("_id"));
			final String contentType = curPart.getString(curPart
					.getColumnIndex("ct"));
			final String fileName = curPart.getString(curPart
					.getColumnIndex("cl"));
			final String text = curPart.getString(curPart
					.getColumnIndex("text"));

			if (LOCAL_LOGV)
				Log.v(TAG, String.format("processing part %s, name=%s (%s)",
						id, fileName, contentType));

			if (contentType.startsWith("text/") && !TextUtils.isEmpty(text)) {
				// text
				parts.add(new MimeBodyPart(new TextBody(text), contentType));
			} else if (contentType.equalsIgnoreCase("application/smil")) {
				// silently ignore SMIL stuff
			} else {
				// attach everything else
				final Uri partUri = Uri.withAppendedPath(
						ServiceBase.MMS_PROVIDER, "part/" + id);
				BodyPart part = new MimeBodyPart(new MmsAttachmentBody(partUri,
						mContext), contentType);
				part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format(
						"%s;\n name=\"%s\"", contentType,
						fileName != null ? fileName : "attachment"));
				part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING,
						"base64");
				part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
						"attachment");

				parts.add(part);
			}
		}

		if (curPart != null)
			curPart.close();
		return parts;
	}

	/**
	 * Create a message-id based on message date, phone number and message type.
	 * 
	 * @param sent
	 *            email send date
	 * @param address
	 *            the email address
	 * @param type
	 *            the type
	 * @return the message-id
	 */
	private String createMessageId(Date sent, String address, int type) {
		try {
			final MessageDigest digest = java.security.MessageDigest
					.getInstance("MD5");

			digest.update(Long.toString(sent.getTime()).getBytes("UTF-8"));
			digest.update(address.getBytes("UTF-8"));
			digest.update(Integer.toString(type).getBytes("UTF-8"));

			final StringBuilder sb = new StringBuilder();
			for (byte b : digest.digest()) {
				sb.append(String.format("%02x", b));
			}
			return String.format(MSG_ID_TEMPLATE, sb.toString());
		} catch (java.io.UnsupportedEncodingException e) {
			throw new RuntimeException(e);
		} catch (java.security.NoSuchAlgorithmException e) {
			throw new RuntimeException(e);
		}
	}

	private static String getHeader(Message msg, String header) {
		try {
			String[] hdrs = msg.getHeader(header);
			if (hdrs != null && hdrs.length > 0) {
				return hdrs[0];
			}
		} catch (MessagingException ignored) {
		}
		return null;
	}

	private String getPrimaryEmail(final long personId, final String number) {
		if (personId <= 0) {
			return getUnknownEmail(number);
		}
		String primaryEmail = null;

		// Get all e-mail addresses for that person.
		Cursor c;
		int columnIndex;
		if (NEW_CONTACT_API) {
			c = mContext
					.getContentResolver()
					.query(
							ECLAIR_CONTENT_URI,
							new String[] { ContactsContract.CommonDataKinds.Email.DATA },
							ContactsContract.CommonDataKinds.Email.CONTACT_ID
									+ " = ?",
							new String[] { String.valueOf(personId) },
							ContactsContract.CommonDataKinds.Email.IS_PRIMARY
									+ " DESC");
			columnIndex = c != null ? c
					.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA)
					: -1;
		} else {
			c = mContext.getContentResolver().query(
					ContactMethods.CONTENT_EMAIL_URI,
					new String[] { ContactMethods.DATA },
					ContactMethods.PERSON_ID + " = ?",
					new String[] { String.valueOf(personId) },
					ContactMethods.ISPRIMARY + " DESC");
			columnIndex = c != null ? c.getColumnIndex(ContactMethods.DATA)
					: -1;
		}

		// Loop over cursor and find a Gmail address for that person.
		// If there is none, pick first e-mail address.
		while (c != null && c.moveToNext()) {
			String e = c.getString(columnIndex);
			if (primaryEmail == null) {
				primaryEmail = e;
			}
			if (isGmailAddress(e)) {
				primaryEmail = e;
				break;
			}
		}

		if (c != null)
			c.close();
		return (primaryEmail != null) ? primaryEmail : getUnknownEmail(number);
	}

	private static String sanitize(String s) {
		return s != null ? s.replaceAll("\\p{Cntrl}", "") : null;
	}

	private static String encodeLocal(String s) {
		return (s != null ? EncoderUtil.encodeAddressLocalPart(sanitize(s))
				: null);
	}

	private static String getUnknownEmail(String number) {
		final String no = (number == null || "-1".equals(number)) ? UNKNOWN_NUMBER
				: number;
		return encodeLocal(no.trim()) + "@" + UNKNOWN_EMAIL;
	}

	// Returns whether the given e-mail address is a Gmail address or not.
	private static boolean isGmailAddress(String email) {
		return email != null
				&& (email.toLowerCase().endsWith("gmail.com") || email
						.toLowerCase().endsWith("googlemail.com"));
	}

	private static String generateReferenceValue() {
		final StringBuilder sb = new StringBuilder();
		final Random random = new Random();
		for (int i = 0; i < 24; i++) {
			sb.append(Integer.toString(random.nextInt(35), 36));
		}
		return sb.toString();
	}

	public static class ConversionResult {
		public final DataType type;
		public final List<Message> messageList = new ArrayList<Message>();
		public final List<Map<String, String>> mapList = new ArrayList<Map<String, String>>();
		public long maxDate = PrefStore.DEFAULT_MAX_SYNCED_DATE;

		public ConversionResult(DataType type) {
			this.type = type;
		}
	}

	public class PersonRecord {
		public long _id;
		public String name, email, number;
		public boolean unknown = false;
		private Address mAddress;

		public Address getAddress() {
			if (mAddress == null) {
				switch (mStyle) {
				case NUMBER:
					mAddress = new Address(email, getNumber());
					break;
				case NAME_AND_NUMBER:
					mAddress = new Address(email, name == null ? getNumber()
							: String.format("%s (%s)", getName(), getNumber()));
					break;
				case NAME:
					mAddress = new Address(email, getName());
					break;
				default:
					mAddress = new Address(email);
				}
			}
			return mAddress;
		}

		public String getId() {
			return unknown ? number : String.valueOf(_id);
		}

		public String getNumber() {
			return sanitize("-1".equals(number) ? "Unknown" : number);
		}

		public String getName() {
			return sanitize(name != null ? name : getNumber());
		}

		public String toString() {
			return String.format("[name=%s email=%s id=%d]", getName(), email,
					_id);
		}
	}

	public static class MmsAttachmentBody implements Body {
		private Context mContext;
		private Uri mUri;

		public MmsAttachmentBody(Uri uri, Context context) {
			mContext = context;
			mUri = uri;
		}

		public InputStream getInputStream() throws MessagingException {
			try {
				return mContext.getContentResolver().openInputStream(mUri);
			} catch (FileNotFoundException fnfe) {
				/*
				 * Since it's completely normal for us to try to serve up
				 * attachments that have been blown away, we just return an
				 * empty stream.
				 */
				return new ByteArrayInputStream(new byte[0]);
			}
		}

		public void writeTo(OutputStream out) throws IOException,
				MessagingException {
			InputStream in = getInputStream();
			Base64OutputStream base64Out = new Base64OutputStream(out);
			IOUtils.copy(in, base64Out);
			base64Out.close();
		}
	}
}
